/* Octopus PDF - A simple document viewer for Android.
   Copyright (C) 2013  Gerald Thaler

   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
package de.geraldthaler.octopuspdf.docrender;

import java.util.ArrayList;

import android.os.Process;
import de.geraldthaler.octopuspdf.Native;
import de.geraldthaler.octopuspdf.func.Proc;
import de.geraldthaler.octopuspdf.func.ProcBoolean;
import de.geraldthaler.octopuspdf.func.ProcInt;
import de.geraldthaler.octopuspdf.func.ProcObj;
import de.geraldthaler.octopuspdf.func.ProcObjObj;
import de.geraldthaler.octopuspdf.gfx.Rect;
import de.geraldthaler.octopuspdf.gfx.Size;
import de.geraldthaler.octopuspdf.gfx.SizeF;
import de.geraldthaler.octopuspdf.memory.Disposable;
import de.geraldthaler.octopuspdf.util.Threading;

/**
 * @author Gerald Thaler
 */
public final class DocRenderer implements Disposable {
    abstract class Request {
        private boolean cancelled;
        private final DocRenderClient client;
        private final int priority;

        Request(final int priority, final DocRenderClient client) {
            this.priority = priority;
            this.client = client;
        }

        void cancel() {
            cancelled = true;
        }

        final DocRenderClient getClient() {
            return client;
        }
        
        final int getPriority() {
            return priority;
        }

        final boolean isCancelled() {
            return cancelled;
        }

        abstract boolean run();
    }
    
    abstract class RenderRequest extends Request {
        public RenderRequest(final DocRenderClient client) {
            super(2, client);
        }

        @Override
        void cancel() {
            if (!isCancelled())
                cancelRendering(this);
            super.cancel();
        }
    }
    
    private abstract class Response implements Disposable {
        private final Request request;

        Response(final Request request) {
            this.request = request;
        }

        @Override
        public void dispose() {
            // nothing
        }
        
        final void invoke() {
            if (!request.isCancelled())
                run();
        }
        
        abstract void run();
    }
    
    private static final int MIN_REQUEST_PRIORITY = 2;

    private SizeF averagePageSize = new SizeF(1f, (float) Math.sqrt(2f));
    private final ArrayList<Request> delayedRequests = new ArrayList<Request>();
    private final DocSource docSource;
    private Request executingRequest = null;
    private long mupdfCtx;
    private final Proc onProgress;
    private SizeF[] pageSizes;
    private int pixmapMemoryUsed = 0;
    private int pixmapMemoryThreshold;
    private boolean rendering = false;
    private boolean quitRequested = false;
    @SuppressWarnings("unchecked")
    private final ArrayList<Request>[] requests = new ArrayList[MIN_REQUEST_PRIORITY + 1];
    private final ArrayList<Response> responses = new ArrayList<Response>();
    private Thread thread;
    {
        for (int prio = 0; prio <= MIN_REQUEST_PRIORITY; ++prio)
            requests[prio] = new ArrayList<Request>();
    }

    public DocRenderer(final DocSource docSource, final Proc onProgress) {
        this.docSource = docSource;
        this.onProgress = onProgress;
    }

    public synchronized void cancelAll(final DocRenderClient client) {
        for (int prio = 0; prio <= MIN_REQUEST_PRIORITY; ++prio) {
            for (final Request request: requests[prio]) {
                if (request.getClient() == client)
                    request.cancel();
            }
        }
        if (executingRequest != null && executingRequest.getClient() == client)
            executingRequest.cancel();
        for (final Request request: delayedRequests) {
            if (request.getClient() == client)
                request.cancel();
        }
    }

    @Override
    public void dispose() {
        if (thread != null) {
            synchronized (this) {
                quitRequested = true;
                notifyAll();
            }
            Threading.joinUninterruptibly(thread);
        }
    }

    public synchronized SizeF getEstimatedPageSize(final int pageNr) {
        final SizeF size = pageSizes[pageNr];
        return size == null ? averagePageSize : size;
    }

    public synchronized SizeF getPageSize(final int pageNr) {
        return pageSizes[pageNr];
    }

    public synchronized void invokeCallbacks() {
        for (final Response response: responses) {
            response.invoke();
            response.dispose();
        }
        responses.clear();
        for (final Request request: delayedRequests)
            addRequest(request);
        delayedRequests.clear();
    }

	public synchronized void requestAuthenticate(final DocRenderClient client, final String password,
			final ProcBoolean callback) {
		addRequest(new Request(0, client) {
            @Override
            public boolean run() {
            	final boolean r;
            	synchronized (DocRenderer.this) {
            		r = Native.authenticatePassword(mupdfCtx, password);
            	}
                addResponse(new Response(this) {
                    @Override
                    void run() {
                        callback.invoke(r);
                    }
                });
                return false;
            }
        });
	}

	public synchronized void requestRender(final DocRenderClient client, final DocRenderTask[] tasks,
            final ProcObjObj<DocRenderTask[], long[]> callback) {
        addRequest(new RenderRequest(client) {
            @Override
            public boolean run() {
                return execRequestRender(this, tasks, callback);
            }
        });
    }

    public synchronized void requestPageCount(final DocRenderClient client,
            final ProcInt callback) {
        addRequest(new Request(0, client) {
            @Override
            public boolean run() {
            	synchronized (DocRenderer.this) {
            		if (pageSizes == null)
            			pageSizes = new SizeF[Native.getPageCount(mupdfCtx)];
            	}
                addResponse(new Response(this) {
                    @Override
                    void run() {
                        callback.invoke(getPageCount());
                    }
                });
                return false;
            }
        });
    }

    public synchronized void requestPasswordNeeded(final DocRenderClient client,
            final ProcBoolean callback) {
        addRequest(new Request(0, client) {
            @Override
            public boolean run() {
            	final boolean r;
            	synchronized (DocRenderer.this) {
            		r = Native.needsPassword(mupdfCtx);
            	}
                addResponse(new Response(this) {
                    @Override
                    void run() {
                        callback.invoke(r);
                    }
                });
                return false;
            }
        });
    }

    public synchronized void requestPageSize(final DocRenderClient client, final int pageNr,
            final ProcObj<SizeF> callback) {
        addRequest(new Request(1, client) {
            @Override
            public boolean run() {
                return execPageSizeRequest(this, pageNr, callback);
            }
        });
    }

    public synchronized void setPixmapMemoryThreshold(final int pixmapMemoryThreshold) {
        this.pixmapMemoryThreshold = pixmapMemoryThreshold; 
    }
    
    public synchronized void start() {
        thread = new Thread(new Runnable() {
            @Override
            public void run() {
                renderMain();
            }
        });
        thread.start();
    }

    void addResponse(final Response response) {
        final boolean empty;
        synchronized (this) {
            empty = responses.isEmpty();
            responses.add(response);
        }
        if (empty)
            onProgress.invoke();
    }

    void cancelRendering(final Request request) {
        if (rendering && executingRequest == request)
            Native.cancelRender(mupdfCtx);
    }

    boolean execRequestRender(final Request request, final DocRenderTask[] tasks,
            final ProcObjObj<DocRenderTask[], long[]> callback) {
        synchronized (this) {
            if (request.isCancelled())
                return false;
            if (pixmapMemoryUsed > pixmapMemoryThreshold)
                return true;
            rendering = true;
            Native.prepareRender(mupdfCtx, tasks.length);
        }
        for (int n = 0; n < tasks.length; ++n) {
            Native.loadPage(mupdfCtx, n, tasks[n].getPageNr());
            final boolean cancelled;
            synchronized (this) {
                cancelled = request.isCancelled();
                if (cancelled)
                    rendering = false;
            }
            if (cancelled) {
                Native.clearMupdf(mupdfCtx);
                return false;
            }
        }
        int memUsed = 0;
        for (int n = 0; n < tasks.length; ++n) {
            final DocRenderTask t = tasks[n];
            final Size s = t.getSize();
            final Rect r = t.getRegion();
            Native.renderPage(mupdfCtx, n, s.getWidth(), s.getHeight(), r.getLeft(), r.getTop(), r.getRight(),
                    r.getBottom());
            final boolean cancelled;
            synchronized (this) {
                cancelled = request.isCancelled();
                if (cancelled)
                    rendering = false;
            }
            if (cancelled) {
                Native.clearMupdf(mupdfCtx);
                return false;
            }
            memUsed += 4 * r.getWidth() * r.getHeight();
        }
        synchronized (this) {
            rendering = false;
            pixmapMemoryUsed += memUsed;
        }
        final long[] pixmaps = new long[tasks.length];
        for (int n = 0; n < tasks.length; ++n)
            pixmaps[n] = Native.fetchPixmap(mupdfCtx, n);
        Native.clearMupdf(mupdfCtx);
        final int finalMemUsed = memUsed;
        addResponse(new Response(request) {
            @Override
            public void dispose() {
                freePixmaps(pixmaps, finalMemUsed);
            }

            @Override
            void run() {
                callback.invoke(tasks, pixmaps);
            }
        });
        return false;
    }

    boolean execPageSizeRequest(final Request request, final int pageNr, final ProcObj<SizeF> callback) {
        SizeF size;
        synchronized (this) {
            size = pageSizes[pageNr];
        }
        if (size == null) {
            Native.loadPage(mupdfCtx, 0, pageNr);
            synchronized (this) {
                final float width = Native.getPageWidth(mupdfCtx, 0);
                final float height = Native.getPageHeight(mupdfCtx, 0);
                size = new SizeF(width, height);
                setPageSize(pageNr, size);
            }
            Native.clearMupdf(mupdfCtx);
        }
        final SizeF r = size;
        addResponse(new Response(request) {
            @Override
            void run() {
                callback.invoke(r);
            }
        });
        return false;
    }

    void freePixmaps(final long[] pixmaps, final int memUsed) {
        for (final long pixmap: pixmaps)
            Native.freePixmap(pixmap);
        pixmapMemoryUsed -= memUsed;
    }

    int getPageCount() {
        return pageSizes.length;
    }

    int getNeedsPassword() {
        return pageSizes.length;
    }

    void renderMain() {
        try {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            renderMain2();
        } catch (final Throwable ex) {
            ex.printStackTrace();
            throw new AssertionError(ex);
        }
    }
    
    private void addRequest(final Request request) {
        requests[request.getPriority()].add(request);
        notifyAll();
    }

    private void checkForQuit() throws QuitException {
        if (quitRequested)
            throw new QuitException();
    }

    private void renderMain2() {
        mupdfCtx = Native.newMupdfCtx(docSource);
        try {
            for (;;) {
                synchronized (this) {
                    outerLoop:
                    for (;;) {
                        checkForQuit();
                        for (int prio = 0; prio <= MIN_REQUEST_PRIORITY; ++prio) {
                            final ArrayList<Request> rl = requests[prio];
                            while (!rl.isEmpty()) {
                                final Request request = rl.remove(0);
                                if (!request.isCancelled()) {
                                    executingRequest = request;
                                    break outerLoop;
                                }
                            }                         
                        }
                        Threading.waitUninterruptibly(this);
                    }
                }
                final boolean delay = executingRequest.run();
                synchronized (this) {
                    if (delay)
                        delayedRequests.add(executingRequest);
                    executingRequest = null;
                }
            }
        } catch (final QuitException ex) {
            // quit
        }
        for (final Response response: responses)
            response.dispose();
        responses.clear();
        Native.deleteMupdfCtx(mupdfCtx);
    }

    private void setPageSize(final int pageNr, SizeF size) {
        pageSizes[pageNr] = size;
        final int pageCount = pageSizes.length;
        float w = 0f;
        float h = 0f;
        float c = 0f;
        for (int n = 0; n < pageCount; ++n) {
            size = pageSizes[n];
            if (size == null)
                continue;
            w += size.getWidth();
            h += size.getHeight();
            ++c;
        }
        averagePageSize = new SizeF(w / c, h / c);
    }
}
